package org.jetbrains.exposed.v1.jdbc.statements

import org.jetbrains.exposed.v1.core.InternalApi
import org.jetbrains.exposed.v1.core.statements.*
import org.jetbrains.exposed.v1.exceptions.ExposedSQLException
import org.jetbrains.exposed.v1.jdbc.JdbcTransaction
import org.jetbrains.exposed.v1.jdbc.statements.api.JdbcPreparedStatementApi
import java.sql.SQLException

internal object DefaultValueMarker {
    override fun toString(): String = "DEFAULT"
}

/**
 * Executable provides a customizable execution mechanism for SQL statements within a transaction.
 *
 * This interface allows implementing classes to define specific execution logic specific to a JDBC driver
 * and customize how the return value is handled.
 * It is primarily used when fine-grained control over statement execution is required.
 *
 * For the suspend R2DBC alternative of this interface, see `SuspendExecutable` provided with a dependency on `exposed-r2dbc`.
 *
 * ## Usage Example:
 * ```kotlin
 * open class BatchUpsertBlockingExecutable(
 *     override val statement: BatchUpsertStatement
 * ) : BatchInsertBlockingExecutable<BatchUpsertStatement>(statement) {
 *     override fun prepared(transaction: JdbcTransaction, sql: String): JdbcPreparedStatementApi {
 *         // We must return values from upsert because returned id could be different depending on insert or upsert happened
 *         if (!currentDialect.supportsOnlyIdentifiersInGeneratedKeys) {
 *             return transaction.connection.prepareStatement(sql, statement.shouldReturnGeneratedValues)
 *         }
 *
 *         return super.prepared(transaction, sql)
 *     }
 * }
 * ```
 *
 * The implemented Executable can be later used in utility functions like `Table.batchUpsert()`.
 *
 * @param T The return type of the SQL execution result.
 * @param S The type of SQL statement that is executed.
 */
interface BlockingExecutable<out T, S : Statement<T>> {
    /** The actual Exposed [Statement] on which the specific execution logic should be used. */
    val statement: S

    /**
     * Determines the exact way that an SQL statement is executed in a [transaction] and applies any necessary
     * logic before returning the result generated by the executed statement.
     */
    fun JdbcPreparedStatementApi.executeInternal(transaction: JdbcTransaction): T?

    /**
     * Uses a [transaction] connection and an [sql] string representation to return a precompiled SQL statement,
     * stored as an implementation of [JdbcPreparedStatementApi].
     */
    fun prepared(
        transaction: JdbcTransaction,
        sql: String
    ): JdbcPreparedStatementApi = transaction.connection.prepareStatement(sql, false)

    /** Whether the SQL statement is meant to be performed as part of a batch execution. */
    val isAlwaysBatch: Boolean
        get() = false

    /**
     * Executes the SQL statement directly in the provided [transaction] and returns the generated result,
     * or `null` if either no result was retrieved or if the transaction blocked statement execution.
     */
    fun execute(transaction: JdbcTransaction): T? {
        return if (transaction.blockStatementExecution) {
            transaction.explainStatement = statement
            null
        } else {
            transaction.exec(this)
        }
    }
}

/**
 * Returns the associated [BlockingExecutable] that accepts this [Statement] type as an argument,
 * allowing the provided statement to be then sent to the database for execution.
 *
 *```kotlin
 * transaction {
 *     val insertTaskStatement = buildStatement {
 *         Tasks.insert {
 *             it[title] = "Follow Exposed tutorial"
 *             it[isComplete] = false
 *         }
 *     }
 *
 *     exec(insertTask.toExecutable())
 * }
 * ```
 *
 * @throws IllegalStateException If the invoking statement does not have a corresponding built-in executable.
 */
fun <T : Any, S : Statement<T>> S.toExecutable(): BlockingExecutable<T, S> {
    @Suppress("UNCHECKED_CAST")
    return when (this) {
        is BatchUpsertStatement -> BatchUpsertBlockingExecutable(this)
        is UpsertStatement<*> -> UpsertBlockingExecutable(this as UpsertStatement<T>)
        is SQLServerBatchInsertStatement -> SQLServerBatchInsertBlockingExecutable(this)
        is BatchInsertStatement -> BatchInsertBlockingExecutable(this)
        is InsertStatement<*> -> InsertBlockingExecutable(this as InsertStatement<T>)
        is BatchUpdateStatement -> BatchUpdateBlockingExecutable(this)
        is UpdateStatement -> UpdateBlockingExecutable(this)
        is DeleteStatement -> DeleteBlockingExecutable(this)
        is InsertSelectStatement -> InsertSelectBlockingExecutable(this)
        is MergeStatement -> MergeBlockingExecutable(this)
        is ReturningStatement -> ReturningBlockingExecutable(this)
        else -> error("An executable could not be associated with ${this::class.qualifiedName}. Pass this statement to a custom executable instance directly.")
    } as BlockingExecutable<T, S>
}

@OptIn(InternalApi::class)
internal fun <T, S : Statement<T>> BlockingExecutable<T, S>.executeIn(
    transaction: JdbcTransaction
): Pair<T?, List<StatementContext>> {
    val arguments = statement.arguments()
    val contexts = if (arguments.any()) {
        arguments.map { args ->
            val context = StatementContext(statement, args)
            JdbcTransaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
            transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
            context
        }
    } else {
        val context = StatementContext(statement, emptyList())
        JdbcTransaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
        transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
        listOf(context)
    }

    val statement = try {
        prepared(transaction, statement.prepareSQL(transaction)).apply {
            timeout = transaction.queryTimeout
        }
    } catch (e: SQLException) {
        throw ExposedSQLException(e, contexts, transaction)
    }
    contexts.forEachIndexed { _, context ->
        statement.fillParameters(context.args)
        // REVIEW
        if (contexts.size > 1 || isAlwaysBatch) statement.addBatch()
    }
    if (!transaction.db.supportsMultipleResultSets) {
        transaction.closeExecutedStatements()
    }

    transaction.currentStatement = statement
    transaction.interceptors.forEach { it.afterStatementPrepared(transaction, statement) }
    val result = try {
        statement.executeInternal(transaction)
    } catch (cause: SQLException) {
        throw ExposedSQLException(cause, contexts, transaction)
    }
    transaction.currentStatement = null
    transaction.executedStatements.add(statement)

    JdbcTransaction.globalInterceptors.forEach { it.afterExecution(transaction, contexts, statement) }
    transaction.interceptors.forEach { it.afterExecution(transaction, contexts, statement) }
    return result to contexts
}
